﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SDCloudSolution.SDCloud.BusinessLayer.Entities;
using SDCloudSolution.SDCloud.BusinessLayer.Model.Interfaces;
using SDCloudSolution.SDCloud.DataLayer.Model.Interfaces;
using SDCloudSolution.SDCloud.Entities.Models;

namespace SDCloudSolution.SDCloud.BusinessLayer.Model
{
    public class ModelStatisticsComponent : IModelStatisticsComponent
    {
        private readonly IModelStatisticsDataProvider _dataProvider;

        public ModelStatisticsComponent(IModelStatisticsDataProvider dataProvider)
        {
            _dataProvider = dataProvider;
        }

        public Task<IEnumerable<ModelStatisticsEntity>> GetModelExecutionStatisticsByModelIdsAsync(IEnumerable<int> modelIds)
        {
            if (modelIds == null)
            {
                throw new ArgumentNullException(nameof(modelIds));
            }

            var evaluatedModelIds = modelIds.ToArray();

            return evaluatedModelIds.Length > 0
                ? _dataProvider.GetByModelIdsAsync(evaluatedModelIds)
                : Task.FromResult(Enumerable.Empty<ModelStatisticsEntity>());
        }

        public Task<ModelStatisticsEntity> GetAggregatedModelStatisticsAsync(IEnumerable<ModelStatisticsEntity> entities)
        {
            var evaluatedEntities = entities.ToArray();

            if (evaluatedEntities.Length == 0)
            {
                return Task.FromResult((ModelStatisticsEntity)null);
            }
                
            return Task.FromResult(new ModelStatisticsEntity
            {
                TotalSuccessRunCount = evaluatedEntities.Sum(entity => entity.TotalSuccessRunCount),
                TotalFailedRunCount = evaluatedEntities.Sum(entity => entity.TotalFailedRunCount),
                MinExecutionTimeInMilliseconds = evaluatedEntities.Min(entity => entity.MinExecutionTimeInMilliseconds),
                MaxExecutionTimeInMilliseconds = evaluatedEntities.Max(entity => entity.MaxExecutionTimeInMilliseconds),
                AvgExecutionTimeInMilliseconds = (long) evaluatedEntities.Average(entity => entity.AvgExecutionTimeInMilliseconds),
                MinDataSizeInBytes = evaluatedEntities.Min(entity => entity.MinDataSizeInBytes),
                MaxDataSizeInBytes = evaluatedEntities.Max(entity => entity.MaxDataSizeInBytes),
                AvgDataSizeInBytes = (long) evaluatedEntities.Average(entity => entity.AvgDataSizeInBytes)
            });
        }

        public async Task LogFailedModelExecutionAsync(int modelId)
        {
            if (modelId <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(modelId), "Index should be greater than zero.");
            }

            var statisticsEntity = await _dataProvider.GetByModelIdAsync(modelId);
            if (statisticsEntity == null || statisticsEntity.ModelId != modelId)
            {
                statisticsEntity = new ModelStatisticsEntity
                {
                    ModelId = modelId,
                    TotalSuccessRunCount = 0,
                    TotalFailedRunCount = 1
                };

                await _dataProvider.InsertAsync(statisticsEntity);
                return;
            }

            statisticsEntity.TotalFailedRunCount++;
            await _dataProvider.UpdateAsync(statisticsEntity);
        }

        public async Task LogSuccessModelExecutionMetricsAsync(ModelExecutionMetrics executionMetrics)
        {
            if (executionMetrics == null)
            {
                throw new ArgumentNullException(nameof(executionMetrics));
            }

            if (executionMetrics.ModelId <= 0)
            {
                throw new ArgumentOutOfRangeException($"{nameof(executionMetrics)}.{nameof(executionMetrics.ModelId)}", "Index should be greater than zero.");
            }

            var statisticsEntity = await _dataProvider.GetByModelIdAsync(executionMetrics.ModelId);
            if (statisticsEntity == null || statisticsEntity.ModelId != executionMetrics.ModelId)
            {
                statisticsEntity = new ModelStatisticsEntity
                {
                    ModelId = executionMetrics.ModelId,
                    TotalFailedRunCount = 0,
                    TotalSuccessRunCount = 1
                };

                // It's the first execution, we can't make dission only on one success execution
                ComputeSuccessStatistics(statisticsEntity, executionMetrics, true);

                await _dataProvider.InsertAsync(statisticsEntity);
                return;
            }

            // It's the first execution, we can't make dission only on one success execution
            var isFirstSuccessExecution = statisticsEntity.TotalSuccessRunCount == 0;
            ComputeSuccessStatistics(statisticsEntity, executionMetrics, isFirstSuccessExecution);
            statisticsEntity.TotalSuccessRunCount++;

            await _dataProvider.UpdateAsync(statisticsEntity);
        }

        private static void ComputeSuccessStatistics(ModelStatisticsEntity statisticsEntity, ModelExecutionMetrics executionMetrics, bool isOverride)
        {
            if (isOverride)
            {
                statisticsEntity.MaxExecutionTimeInMilliseconds = executionMetrics.TimeExecutionInMilliseconds;
                statisticsEntity.MinExecutionTimeInMilliseconds = executionMetrics.TimeExecutionInMilliseconds;
                statisticsEntity.AvgExecutionTimeInMilliseconds = executionMetrics.TimeExecutionInMilliseconds;

                statisticsEntity.MaxDataSizeInBytes = executionMetrics.UsedDataSizeInBytes;
                statisticsEntity.MinDataSizeInBytes = executionMetrics.UsedDataSizeInBytes;
                statisticsEntity.AvgDataSizeInBytes = executionMetrics.UsedDataSizeInBytes;
            }
            else
            {
                statisticsEntity.MaxExecutionTimeInMilliseconds = Math.Max(statisticsEntity.MaxExecutionTimeInMilliseconds, executionMetrics.TimeExecutionInMilliseconds);
                statisticsEntity.MinExecutionTimeInMilliseconds = Math.Min(statisticsEntity.MinExecutionTimeInMilliseconds, executionMetrics.TimeExecutionInMilliseconds);
                statisticsEntity.AvgExecutionTimeInMilliseconds = ComputeAverage(
                        statisticsEntity.AvgExecutionTimeInMilliseconds,
                        executionMetrics.TimeExecutionInMilliseconds, 
                        statisticsEntity.TotalSuccessRunCount + 1
                    );

                statisticsEntity.MaxDataSizeInBytes = Math.Max(statisticsEntity.MaxDataSizeInBytes, executionMetrics.UsedDataSizeInBytes);
                statisticsEntity.MinDataSizeInBytes = Math.Min(statisticsEntity.MinDataSizeInBytes, executionMetrics.UsedDataSizeInBytes);
                statisticsEntity.AvgDataSizeInBytes = ComputeAverage(
                        statisticsEntity.AvgDataSizeInBytes,
                        executionMetrics.UsedDataSizeInBytes,
                        statisticsEntity.TotalSuccessRunCount + 1
                    );
            }
        }

        private static long ComputeAverage(long previousAverage, long newValue, int totalCount)
        {
            if (totalCount <= 1)
            {
                return newValue;
            }

            var count = Math.Min(5, totalCount);
            return previousAverage + (newValue - previousAverage) / count;
        }

        public Task<ModelStatisticsEntity> GetModelExecutionStatisticsByModelIdAsync(int modelId)
        {
            if (modelId <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(modelId), "Index should be greater than zero.");
            }

            return _dataProvider.GetByModelIdAsync(modelId);
        }

        protected Task InsertModelStatisticsAsync(ModelStatisticsEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            return _dataProvider.InsertAsync(entity);
        }

        protected Task UpdateModelStatisticsAsync(ModelStatisticsEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            if (entity.ModelId <= 0)
            {
                throw new ArgumentOutOfRangeException($"{nameof(entity)}.{nameof(entity.ModelId)}", "Index should be greater than zero.");
            }

            return _dataProvider.UpdateAsync(entity);
        }
    }
}
